package cn.tedu.charging.cost.service.impl;

import cn.tedu.charging.common.pojo.param.ChargingProcessParam;
import cn.tedu.charging.common.pojo.vo.ChargingProcessVO;
import cn.tedu.charging.cost.dao.repository.ChargingDataRepository;
import cn.tedu.charging.cost.dao.repository.CostRuleCacheRepository;
import cn.tedu.charging.cost.dao.repository.CostRuleRepository;
import cn.tedu.charging.cost.pojo.ChargingDataPO;
import cn.tedu.charging.cost.pojo.po.CostRulePO;
import cn.tedu.charging.cost.service.CostService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;

@Slf4j
@Service
public class CostServiceImpl implements CostService {

    /**
     * key 订单号,value 数字 ,不同订单号对应不同的次数
     * 1 独享数据问题 需要改成共享
     * 2 数据量过大问题 我们业务服务器配置 一般  4C 8G  不重启 一直运行了一年 1000个订单 * 365 = 365000 数据量太大导致内存溢出,
     * 3 重启数据会丢 ,业务出问题
     *  我们的订单给计价同步的数据 订单会存储到共享的ES
     */
    private Map<String, ChargingDataPO> chargingProcessData = new HashMap<>();
    /**
     * 计算价格
     * @param chargingProcessParam
     * @return
     */
    @Override
    public ChargingProcessVO calculateCost(ChargingProcessParam chargingProcessParam) {
        //同一订单的 价格的累加 上次的价格 + 当前的价格
        //累加 上次 + 当前
        //怎么区分次数 第1次 或者 是 第N次
        //有个依据 来判断 是第几次 标准
        //标准存储  怎么存? 按照订单号来区分 每个订单 是第一次 还是 第N次
        //Integer count = 0; 局部变量,每次calculateCost调用都是0 无法累加
        //通过chargingProcessParam 入参订单号,获取充电数据,如果有就返回,没有就初始化,并且放入map
        ChargingDataPO chargingData = getChargingDataByES(chargingProcessParam);
        log.debug("通过订单号:{} 获取充电数据:{}",chargingProcessParam.getOrderNo(),chargingData);
        //获取当前时间 小时
        int hour = getHour();
        //通过当前小时设备同步数据的时间,来获取现在的小时属于哪个时间段
        log.debug("通过 当前时间小时:{} 获取计价规则",hour);
        //通过计价规则计算价格 高峰(计价名称) 2(度数) * 10(当前电费) = 20
        //通过当前时间小时匹配计价规则
        //获取场站信息
        Integer stationId = chargingProcessParam.getStationId();
        CostRulePO costRule =  getMatchCostRuleByHour(stationId,hour);
        log.debug("通过 当前时间小时:{} 获取计价规则:{}",hour,costRule);
        if (costRule != null) {
            //获取计价名称
            String name = costRule.getName();
            //每度电的价格,当前计价规则的每度电的价格
            BigDecimal powerFee = costRule.getPowerFee();
            //判断是否第一次
            if (chargingData.getLastChargingProcess() == null) {
                //第一次
                firstCost2ChargingData(chargingProcessParam,chargingData,hour,name,powerFee);
            }else {
                //不是第一次
                lastCost2ChargingData(chargingProcessParam,chargingData,hour,name,powerFee);
            }
        }else {
            log.error("场站信息:{},当前时间:{}获取计价规则失败,告警告警告警,接入告警系统",stationId,hour);
        }
        //3 累加
        //方案淘汰 1:3平  2:1尖  3:2峰 每次同步的是 实际充了多少度电 设备需要记录每次充了多少度电  3 + 1 + 2 = 6 ,必须一次都不能丢
        //方案胜出 1:3平  2:4尖  3:6峰 每次同步的是 已经充了多少度电 设备需要记录已经充了多少度电  6  某次同步丢失 问题不大
        //[3*平的价格] + [(4-3) * 尖的价格] + [(6-4) * 峰的价格] = 总价
        float chargingCapacity = chargingProcessParam.getChargingCapacity();
        //记录充电总度数
        chargingData.setTotalCapacity(chargingCapacity);
        log.debug("当前充电数据记录:{}",chargingData);
        return buildChargingProcessVo(chargingData);

        //用户充电需要一定的时间, 整体的充电时间可能跨越多个时间段
        //计价规则
        //计价规则的管理 基于<场站>的维度进行管理
        // 计价规则属于谁? 谁的计价规则 <场站>
        // 全国一个计价规则?
        // 市区一个计价规则?
        // 公司一个计价规则?

        //结论 一个场站一个计价规则

        //设计一个计价规则表
        // Id(主键), stationId(场站id),规则名称,价格,开始时间,结束时间,枪类型
        //  1         1                高峰   10   12:00   14:00  快
        //  2         1                高峰   5    12:00   14:00  慢
        //  3         1                平峰   8    10:00   12:00  快
        //  4         1                平峰   4    10:00   12:00  慢



        //10:00-18:00  8个小时
        //时间段
        // 高峰 12:00-14:00 快充 10 慢充 5
        // 平峰 10:00-12:00 快充 8 慢充  4
        // 低峰 14:00-18:00 快充 6 慢充  3

        //快充花费多少钱 ?
        // 40 ? 60 ?
        // 高峰 2 * 10 = 20
        // 平峰 2 * 8  = 16
        // 低峰 4 * 6  = 24
        // 总价 20 + 16 + 24 = 60

        // 高峰半小时,低峰半小时 时长和价格  价格 同步的时间属于哪个时间段?
        //11:00-12:00 高峰
        //12:00-13:00 低峰
        //11:30 12:30 高峰半小时 低峰半小时 设备在和订单服务同步数据
        //11:30:01 同步一次 高
        //11:30:02 同步一次 高
        //11:30:03 同步一次 高
        //11:59:59 同步一次 高
        //12:00:00 同步一次 低
    }

    private ChargingProcessVO buildChargingProcessVo(ChargingDataPO chargingData) {
        ChargingProcessVO chargingProcessVO = new ChargingProcessVO();

        //订单编号
        chargingProcessVO.setOrderNo(chargingProcessVO.getOrderNo());
        //总花费
        chargingProcessVO.setTotalCost(chargingData.getTotalCost());
        //总度数
        chargingProcessVO.setTotalCapacity(chargingData.getTotalCapacity());
        //总时长
        //第一次充电的时间
        Long startTime = chargingData.getStartTime();
        //当前时间 todo 时间范围计算 自己了解
        LocalDateTime now = LocalDateTime.now();
        //Duration between = Duration.between(startTime, now);
        //String totalTime = between.toDays() + "-" +
        //        between.toHours() + "-" +
        //        between.toMinutes() + "-" +
        //        between.getSeconds();
        //chargingProcessVO.setTotalTime(totalTime);
        //todo 自己计算 充电总花费 除以 充电总度数 每度电的价格
        //chargingProcessVO.setPowerFee();
        //用户id
        chargingProcessVO.setUserId(chargingData.getUserId());
        //设备信息
        chargingProcessVO.setGunId(chargingProcessVO.getGunId());


        //如果参数多,可以 BeanUtils.copyProperties 来赋值
        //需要注意变量名称和类型
        //1获取源(source)类获取读方法 getter ,获取目标(target)类写方法
        //2 判断方法如果不是public setAccessible(true) //强制访问
        //3 writeMethod.invoke(target, value); 写方法调用 入参1 目标 入参2 值
        // target.writeMethod(value) --> chargingProcessVO.setXXX(value)
        //BeanUtils.copyProperties(chargingData,chargingProcessVO);
        return chargingProcessVO;

    }

    /**
     * 获取当前时间 小时
     * @return
     */
    private int getHour() {
        //获取当前时间
        LocalDateTime now = LocalDateTime.now();
        //获取当前小时 14
        return  now.getHour();
    }

    /**
     *
     * 第一次的充电同步的数据Param到ChargingData
     */
    private void firstCost2ChargingData(ChargingProcessParam chargingProcessParam,
                                        ChargingDataPO chargingData,
                                        Integer hour,
                                        String name,
                                        BigDecimal powerFee) {
        log.debug("第一次同步充电数据");
        //获取充电度数
        float chargingCapacity = chargingProcessParam.getChargingCapacity();
        //本次花费 充电度数 * 每度电的价格
        BigDecimal chargingCost = getCost(chargingCapacity, powerFee);
        log.debug("当前时间(小时):{},计价规则名称:{},充电度数:{},电费单价:{},本次花费:{}",
                hour,name,chargingCapacity,powerFee,chargingCost);
        //保存当前总花费到 chargingData //第1笔钱 上课 打车  20
        chargingData.setTotalCost(chargingCost);
        //保存当前充电记录到chargingData, 为了下一次的计算做准备
        chargingData.setLastChargingProcess(chargingProcessParam);
        //注意 如果用es setLastChargingProcess方法只是修改了chargingData的数据
        //如果不通过 chargingDataRepository.save(chargingData); 是不会持久化的
        //每次改变都需要持久化
        log.debug("第一次同步充电数据,保存充电进度到ES:{}",chargingData);
        chargingDataRepository.save(chargingData);
    }

    /**
     * 第N次的充电同步的数据Param到ChargingData
     */
    private void lastCost2ChargingData(ChargingProcessParam chargingProcessParam, ChargingDataPO chargingData,
                                       Integer hour,
                                       String name,
                                       BigDecimal powerFee) {
        //计算本次的
        //取出上次
        //本次的 + 上次的
        log.debug("不是第一次同步充电数据");
        float realChargingCapacity = getRealChargingCapacity(chargingProcessParam, chargingData);
        //本次花费 充电度数 * 每度电的价格 //第2笔钱 吃早点 5  第3笔钱 吃午餐 50
        BigDecimal currentChargingCost = getCost(realChargingCapacity, powerFee);
        log.debug("当前时间(小时):{},计价规则名称:{},充电度数:{},电费单价:{},本次花费:{}",
                hour,name,realChargingCapacity,powerFee,currentChargingCost);
        //取上次的花费 第1笔钱 打车 20
        BigDecimal totalCost = chargingData.getTotalCost();
        //累加 当前总花费 = 上次花费 + 本次花费
        //  第1笔钱 打车 20 + 第2笔钱 吃早点 5  = 25
        // 上两次的总花费 25 + 吃午餐 50 = 75
        BigDecimal currentTotalCost = totalCost.add(currentChargingCost);
        //保存当前总花费到 chargingData
        chargingData.setTotalCost(currentTotalCost);
        log.debug("当前时间(小时):{},计价规则名称:{},充电度数:{},电费单价:{},本次花费:{},总花费:{}",
                hour,name,realChargingCapacity,powerFee,currentChargingCost,currentTotalCost);
        //保存当前充电记录到chargingData, 为了下一次的计算做准备
        chargingData.setLastChargingProcess(chargingProcessParam);
        //注意 如果用es setLastChargingProcess方法只是修改了chargingData的数据
        //如果不通过 chargingDataRepository.save(chargingData); 是不会持久化的
        //每次改变都需要持久化
        log.debug("不是第一次同步充电数据,保存充电进度到ES:{}",chargingData);
        chargingDataRepository.save(chargingData);
    }


    @Autowired
    private ChargingDataRepository chargingDataRepository;



    /**
     * 通过订单号获取充电数据
     * @return
     */
    private ChargingDataPO getChargingDataByES(ChargingProcessParam  param){
        //从ES获取充电数据
        Optional<ChargingDataPO> optional = chargingDataRepository
                .findById(param.getOrderNo());
        if (optional.isPresent()) {
            //第N次同步 从Map获取直接返回
            return optional.get();
        }else {
            return initESChargingData(param);
        }
    }
    /**
     * 通过订单号获取充电数据
     * @return
     */
    private ChargingDataPO getChargingData(ChargingProcessParam  param){
        //从map获取充电数据
        ChargingDataPO chargingData = chargingProcessData.get(param.getOrderNo());
        //判断es里是否有当前订单号
        //SearchPage chargingProcessByOrderNo = chargingProcessRepository.
        //        getChargingProcessByOrderNo(param.getOrderNo());


        //判断充电数据是否为空
        if(chargingData == null){
            //第一次同步 充电数据为空,初始化一个充电记录数据,存入map
            chargingData = initChargingData(param);
        }
        //第N次同步 从Map获取直接返回
        return chargingData;
    }

    /**
     * 第一次同步充电数据,
     * 创建数据同步记录对象 ChargingData
     * 设置 订单信息 设备信息 用户信息
     * @param param
     * @return
     */
    private ChargingDataPO initChargingData(ChargingProcessParam param){
        //第一次 初始化 充电数据,为了下一次同步的时候,获取上一次
        ChargingDataPO chargingData = new ChargingDataPO();
        chargingData.setOrderNo(param.getOrderNo());
        chargingData.setStartTime(System.currentTimeMillis());
        chargingData.setGunId(param.getGunId());
        chargingData.setUserId(param.getUserId());
        chargingData.setCount(1);
        //保存初始化数据到map;
        chargingProcessData.put(param.getOrderNo(),chargingData);
        return chargingData;
    }

    /**
     * 第一次同步充电数据,订单号为ES的id
     * 创建数据同步记录对象 ChargingData
     * 设置 订单信息 设备信息 用户信息
     * @param param
     * @return
     */
    private ChargingDataPO initESChargingData(ChargingProcessParam param){
        //第一次 初始化 充电数据,为了下一次同步的时候,获取上一次
        ChargingDataPO chargingData = new ChargingDataPO();
        //要设置订单号到id
        chargingData.setId(param.getOrderNo());
        chargingData.setOrderNo(param.getOrderNo());
        chargingData.setStartTime(System.currentTimeMillis());
        chargingData.setGunId(param.getGunId());
        chargingData.setUserId(param.getUserId());
        chargingData.setCount(1);
        //保存初始化数据到ES
        chargingDataRepository.save(chargingData);
        return chargingData;
    }

    /**
     * 获取真实充电度数
     * 1:3平  3       第一次 传充电度数返回充电度数
     * 2:4尖  4-3 = 1 第二次 传的充电度数 - 上一次充电度数
     * 3:6峰  6-4 = 2 第三次 传的充电度数 - 上一次充电度数
     *
     * @return
     */
    private float getRealChargingCapacity(ChargingProcessParam chargingProcessParam, ChargingDataPO chargingData) {
        //上一次充电的记录
        ChargingProcessParam lastChargingProcess = chargingData.getLastChargingProcess();
        //上一次充电的度数
        float chargingCapacity = lastChargingProcess.getChargingCapacity();
        //取这次充电度数
        float currentChargingCapacity = chargingProcessParam.getChargingCapacity();
        //因为身边同步的充电度数是包含关系, 一共充了多少度电
        // 实际的充电度数 等于 一共度数 减去 上次度数
        float realChargingCapacity =  currentChargingCapacity - chargingCapacity;
        return realChargingCapacity;
    }

    /**
     * 计算价格  度数 * 单价
     * @param chargingCapacity
     * @param fee
     * @return
     */
    private BigDecimal getCost(float chargingCapacity,BigDecimal fee) {
        //float与double的区别 https://m.ha.huatu.com/2022/0328/2573630.html
        //把float类型的充电度数转换为字符串类型
        String stringChargingCapacity = Float.toString(chargingCapacity);
        //把字符串类型的充电度数转换为BigDecimal
        BigDecimal bigDecimalChargingCapacity = new BigDecimal(stringChargingCapacity);
        // 电费 * 充电度数
        return fee.multiply(bigDecimalChargingCapacity);
    }


    /**
     * 通过时间获取匹配的计价规则
     * @param hour
     * @return
     */
    private CostRulePO getMatchCostRuleByHour(Integer stationId,int hour) {
        log.debug("通过场站id:{},获取计价规则列表", stationId);
        List<CostRulePO> costRulePOS = getCostRules(stationId);
        log.debug("通过场站id:{},获取计价规则列表,结果:{}", stationId,costRulePOS);
        for (CostRulePO costRulePO : costRulePOS) {
            //<= 还是 <
            //11-12 高
            //12-13 低
            //12 应该是 高 还是低
            // hour >= costRulePO.getStartTime() && hour < costRulePO.getEndTime()
            //11-12 低
            //12-13 高
            //12 应该是 高 还是低
            // hour >= costRulePO.getStartTime() && hour < costRulePO.getEndTime()
            //判断当前小时属于哪个时间段
            //当前小时 大于等于计价规则的开始时间 并且 小于 结束时间
            if (hour >= costRulePO.getStartTime() && hour < costRulePO.getEndTime()) {
                return costRulePO;
            }
        }
        //CostRulePO result = null;
        //final CostRulePO result = null;
        //final CostRulePO[] result = {new CostRulePO()};
        //costRulePOS.forEach(costRulePO -> {
        //    if (hour >= costRulePO.getStartTime() && hour < costRulePO.getEndTime()) {
        //        result[0]  = costRulePO;
        //    }
        //});
        //return result;
        return null;
    }

    @Autowired
    private CostRuleRepository costRuleRepository;

    @Autowired
    private CostRuleCacheRepository costRuleCacheRepository;


    /**
     * 通过场站信息获取计价规则
     * 计价规则 是存储在数据库的 会被高频的读取
     * 计价规则数据的特点
     * 1 会被高频读取
     * 2 不会频繁的变化
     * 这两种特点的数据 适合放入到redis
     * 库存可以放到redis么?
     * 1 会被高频读取? 会
     * 2 频繁的变化么? 会
     *
     * 普通电商的库存数据 不建议放到redis 选择了一致性和事务
     * 1 redis 不支持事务 支持不太行 相对与mysql
     * 2 库存在数据库存一份,如果放到redis 需要数据库和redis保持一致 非常难搞
     *
     * 秒杀价格
     * 库存放入到redis 选择了高性能,忽略了一致性和事务
     *
     * 大名鼎鼎的 数据库和缓存使用的模式  数据库和redis怎么配合使用,解决我们的业务问题,提升查询计价规则的性能
     *  Cache Aside模式 旁路缓存
     *
     * @param stationId
     * @return
     */
    private List<CostRulePO> getCostRules(Integer stationId) {
        log.debug("通过站点id:{},从redis获取计价规则",stationId);
        List<CostRulePO> cacheCostRules = costRuleCacheRepository.getCostRuleByStationId(stationId);
        log.debug("通过站点id:{},从redis获取计价规则:{}",stationId,cacheCostRules);
        //判断Redis是否有数据
        if (!CollectionUtils.isEmpty(cacheCostRules)){
            log.debug("通过站点id:{},从redis获取计价规则,redis有数据,直接返回",stationId);
            return cacheCostRules;
        }else {
            log.debug("通过站点id:{},从redis获取计价规则,redis没有查询到数据,从数据库查询",stationId);
            List<CostRulePO> dbCostRules = costRuleRepository.getCostRules(stationId);
            //判断数据库是否有数据
            if (!CollectionUtils.isEmpty(dbCostRules)){
                log.debug("通过站点id:{},从数据库获取计价规则,数据库有数据,写入Redis,并返回",stationId);
                costRuleCacheRepository.saveCostRule(stationId,dbCostRules);
                return dbCostRules;
            }
        }
        return Collections.emptyList();
    }
}
